summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt1064
1 files changed, 1064 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
new file mode 100644
index 000000000..c9f5797ac
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -0,0 +1,1064 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.app.Activity
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.VectorDrawable
+import android.os.Build
+import android.util.AttributeSet
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
+import android.view.SurfaceView
+import android.view.View
+import android.view.View.OnTouchListener
+import android.view.WindowInsets
+import androidx.core.content.ContextCompat
+import androidx.preference.PreferenceManager
+import androidx.window.layout.WindowMetricsCalculator
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
+import org.yuzu.yuzu_emu.NativeLibrary.StickType
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Draws the interactive input overlay on top of the
+ * [SurfaceView] that is rendering emulation.
+ */
+class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs),
+ OnTouchListener {
+ private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
+ private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
+ private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
+
+ private var inEditMode = false
+ private var buttonBeingConfigured: InputOverlayDrawableButton? = null
+ private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
+ private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
+
+ private lateinit var windowInsets: WindowInsets
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+
+ windowInsets = rootWindowInsets
+
+ if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
+ defaultOverlay()
+ }
+
+ // Load the controls.
+ refreshControls()
+
+ // Set the on touch listener.
+ setOnTouchListener(this)
+
+ // Force draw
+ setWillNotDraw(false)
+
+ // Request focus for the overlay so it has priority on presses.
+ requestFocus()
+ }
+
+ override fun draw(canvas: Canvas) {
+ super.draw(canvas)
+ for (button in overlayButtons) {
+ button.draw(canvas)
+ }
+ for (dpad in overlayDpads) {
+ dpad.draw(canvas)
+ }
+ for (joystick in overlayJoysticks) {
+ joystick.draw(canvas)
+ }
+ }
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ if (inEditMode) {
+ return onTouchWhileEditing(event)
+ }
+
+ var shouldUpdateView = false
+ val playerIndex =
+ if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
+
+ for (button in overlayButtons) {
+ if (!button.updateStatus(event)) {
+ continue
+ }
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ button.buttonId,
+ button.status
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ for (dpad in overlayDpads) {
+ if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) {
+ continue
+ }
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.upId,
+ dpad.upStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.downId,
+ dpad.downStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.leftId,
+ dpad.leftStatus
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ dpad.rightId,
+ dpad.rightStatus
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ for (joystick in overlayJoysticks) {
+ if (!joystick.updateStatus(event)) {
+ continue
+ }
+ val axisID = joystick.joystickId
+ NativeLibrary.onGamePadJoystickEvent(
+ playerIndex,
+ axisID,
+ joystick.xAxis,
+ joystick.realYAxis
+ )
+ NativeLibrary.onGamePadButtonEvent(
+ playerIndex,
+ joystick.buttonId,
+ joystick.buttonStatus
+ )
+ playHaptics(event)
+ shouldUpdateView = true
+ }
+
+ if (shouldUpdateView)
+ invalidate()
+
+ if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) {
+ return true
+ }
+
+ val pointerIndex = event.actionIndex
+ val xPosition = event.getX(pointerIndex).toInt()
+ val yPosition = event.getY(pointerIndex).toInt()
+ val pointerId = event.getPointerId(pointerIndex)
+ val motionEvent = event.action and MotionEvent.ACTION_MASK
+ val isActionDown =
+ motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+ val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
+ val isActionUp =
+ motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+
+ if (isActionDown && !isTouchInputConsumed(pointerId)) {
+ NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
+ }
+
+ if (isActionMove) {
+ for (i in 0 until event.pointerCount) {
+ val fingerId = event.getPointerId(i)
+ if (isTouchInputConsumed(fingerId)) {
+ continue
+ }
+ NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i))
+ }
+ }
+
+ if (isActionUp && !isTouchInputConsumed(pointerId)) {
+ NativeLibrary.onTouchReleased(pointerId)
+ }
+
+ return true
+ }
+
+ private fun playHaptics(event: MotionEvent) {
+ if (EmulationMenuSettings.hapticFeedback) {
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP ->
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
+ }
+ }
+ }
+
+ private fun isTouchInputConsumed(track_id: Int): Boolean {
+ for (button in overlayButtons) {
+ if (button.trackId == track_id) {
+ return true
+ }
+ }
+ for (dpad in overlayDpads) {
+ if (dpad.trackId == track_id) {
+ return true
+ }
+ }
+ for (joystick in overlayJoysticks) {
+ if (joystick.trackId == track_id) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun onTouchWhileEditing(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val fingerPositionX = event.getX(pointerIndex).toInt()
+ val fingerPositionY = event.getY(pointerIndex).toInt()
+
+ // TODO: Provide support for portrait layout
+ //val orientation =
+ // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
+
+ for (button in overlayButtons) {
+ // Determine the button state to apply based on the MotionEvent action flag.
+ when (event.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ // If no button is being moved now, remember the currently touched button to move.
+ if (buttonBeingConfigured == null &&
+ button.bounds.contains(
+ fingerPositionX,
+ fingerPositionY
+ )
+ ) {
+ buttonBeingConfigured = button
+ buttonBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
+ buttonBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
+ // Persist button position by saving new place.
+ saveControlPosition(
+ buttonBeingConfigured!!.buttonId,
+ buttonBeingConfigured!!.bounds.centerX(),
+ buttonBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ buttonBeingConfigured = null
+ }
+ }
+ }
+
+ for (dpad in overlayDpads) {
+ // Determine the button state to apply based on the MotionEvent action flag.
+ when (event.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN ->
+ // If no button is being moved now, remember the currently touched button to move.
+ if (buttonBeingConfigured == null &&
+ dpad.bounds.contains(fingerPositionX, fingerPositionY)
+ ) {
+ dpadBeingConfigured = dpad
+ dpadBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
+ dpadBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
+ // Persist button position by saving new place.
+ saveControlPosition(
+ dpadBeingConfigured!!.upId,
+ dpadBeingConfigured!!.bounds.centerX(),
+ dpadBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ dpadBeingConfigured = null
+ }
+ }
+ }
+
+ for (joystick in overlayJoysticks) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
+ joystick.bounds.contains(
+ fingerPositionX,
+ fingerPositionY
+ )
+ ) {
+ joystickBeingConfigured = joystick
+ joystickBeingConfigured!!.onConfigureTouch(event)
+ }
+
+ MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
+ joystickBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ }
+
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
+ saveControlPosition(
+ joystickBeingConfigured!!.buttonId,
+ joystickBeingConfigured!!.bounds.centerX(),
+ joystickBeingConfigured!!.bounds.centerY(),
+ ""
+ )
+ joystickBeingConfigured = null
+ }
+ }
+ }
+
+ return true
+ }
+
+ private fun addOverlayControls(orientation: String) {
+ val windowSize = getSafeScreenSize(context)
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_a,
+ R.drawable.facebutton_a_depressed,
+ ButtonType.BUTTON_A,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_b,
+ R.drawable.facebutton_b_depressed,
+ ButtonType.BUTTON_B,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_x,
+ R.drawable.facebutton_x_depressed,
+ ButtonType.BUTTON_X,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_y,
+ R.drawable.facebutton_y_depressed,
+ ButtonType.BUTTON_Y,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.l_shoulder,
+ R.drawable.l_shoulder_depressed,
+ ButtonType.TRIGGER_L,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.r_shoulder,
+ R.drawable.r_shoulder_depressed,
+ ButtonType.TRIGGER_R,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.zl_trigger,
+ R.drawable.zl_trigger_depressed,
+ ButtonType.TRIGGER_ZL,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.zr_trigger,
+ R.drawable.zr_trigger_depressed,
+ ButtonType.TRIGGER_ZR,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_plus,
+ R.drawable.facebutton_plus_depressed,
+ ButtonType.BUTTON_PLUS,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_minus,
+ R.drawable.facebutton_minus_depressed,
+ ButtonType.BUTTON_MINUS,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) {
+ overlayDpads.add(
+ initializeOverlayDpad(
+ context,
+ windowSize,
+ R.drawable.dpad_standard,
+ R.drawable.dpad_standard_cardinal_depressed,
+ R.drawable.dpad_standard_diagonal_depressed,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) {
+ overlayJoysticks.add(
+ initializeOverlayJoystick(
+ context,
+ windowSize,
+ R.drawable.joystick_range,
+ R.drawable.joystick,
+ R.drawable.joystick_depressed,
+ StickType.STICK_L,
+ ButtonType.STICK_L,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) {
+ overlayJoysticks.add(
+ initializeOverlayJoystick(
+ context,
+ windowSize,
+ R.drawable.joystick_range,
+ R.drawable.joystick,
+ R.drawable.joystick_depressed,
+ StickType.STICK_R,
+ ButtonType.STICK_R,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_home,
+ R.drawable.facebutton_home_depressed,
+ ButtonType.BUTTON_HOME,
+ orientation
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.facebutton_screenshot,
+ R.drawable.facebutton_screenshot_depressed,
+ ButtonType.BUTTON_CAPTURE,
+ orientation
+ )
+ )
+ }
+ }
+
+ fun refreshControls() {
+ // Remove all the overlay buttons from the HashSet.
+ overlayButtons.clear()
+ overlayDpads.clear()
+ overlayJoysticks.clear()
+ val orientation =
+ if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
+
+ // Add all the enabled overlay items back to the HashSet.
+ if (EmulationMenuSettings.showOverlay) {
+ addOverlayControls(orientation)
+ }
+ invalidate()
+ }
+
+ private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
+ val windowSize = getSafeScreenSize(context)
+ val min = windowSize.first
+ val max = windowSize.second
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
+ .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
+ .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
+ .apply()
+ }
+
+ fun setIsInEditMode(editMode: Boolean) {
+ inEditMode = editMode
+ }
+
+ private fun defaultOverlay() {
+ if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
+ defaultOverlayLandscape()
+ }
+
+ resetButtonPlacement()
+ preferences.edit()
+ .putBoolean(Settings.PREF_OVERLAY_INIT, true)
+ .apply()
+ }
+
+ fun resetButtonPlacement() {
+ defaultOverlayLandscape()
+ refreshControls()
+ }
+
+ private fun defaultOverlayLandscape() {
+ // Each value represents the position of the button in relation to the screen size without insets.
+ preferences.edit()
+ .putFloat(
+ ButtonType.BUTTON_A.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_A.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_B.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_B.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_X.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_X.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_Y.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_Y.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZL.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZL.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZR.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_ZR.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.DPAD_UP.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.DPAD_UP.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_L.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_L.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_R.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.TRIGGER_R.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_PLUS.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_PLUS.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_MINUS.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_MINUS.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_HOME.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_HOME.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_CAPTURE.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
+ .toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.BUTTON_CAPTURE.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
+ .toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_R.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_R.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_L.toString() + "-X",
+ resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
+ )
+ .putFloat(
+ ButtonType.STICK_L.toString() + "-Y",
+ resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
+ )
+ .apply()
+ }
+
+ override fun isInEditMode(): Boolean {
+ return inEditMode
+ }
+
+ companion object {
+ private val preferences: SharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ /**
+ * Resizes a [Bitmap] by a given scale factor
+ *
+ * @param context Context for getting the vector drawable
+ * @param drawableId The ID of the drawable to scale.
+ * @param scale The scale factor for the bitmap.
+ * @return The scaled [Bitmap]
+ */
+ private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
+ val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
+
+ val bitmap = Bitmap.createBitmap(
+ (vectorDrawable.intrinsicWidth * scale).toInt(),
+ (vectorDrawable.intrinsicHeight * scale).toInt(),
+ Bitmap.Config.ARGB_8888
+ )
+
+ val dm = context.resources.displayMetrics
+ val minScreenDimension = min(dm.widthPixels, dm.heightPixels)
+
+ val maxBitmapDimension = max(bitmap.width, bitmap.height)
+ val bitmapScale = scale * minScreenDimension / maxBitmapDimension
+
+ val scaledBitmap = Bitmap.createScaledBitmap(
+ bitmap,
+ (bitmap.width * bitmapScale).toInt(),
+ (bitmap.height * bitmapScale).toInt(),
+ true
+ )
+
+ val canvas = Canvas(scaledBitmap)
+ vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
+ vectorDrawable.draw(canvas)
+ return scaledBitmap
+ }
+
+ /**
+ * Gets the safe screen size for drawing the overlay
+ *
+ * @param context Context for getting the window metrics
+ * @return A pair of points, the first being the top left corner of the safe area,
+ * the second being the bottom right corner of the safe area
+ */
+ private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
+ // Get screen size
+ val windowMetrics =
+ WindowMetricsCalculator.getOrCreate()
+ .computeCurrentWindowMetrics(context as Activity)
+ var maxY = windowMetrics.bounds.height().toFloat()
+ var maxX = windowMetrics.bounds.width().toFloat()
+ var minY = 0
+ var minX = 0
+
+ // If we have API access, calculate the safe area to draw the overlay
+ var cutoutLeft = 0
+ var cutoutBottom = 0
+ val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
+ if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
+ insets.boundingRectTop.bottom.toFloat() else maxY
+ if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
+ insets.boundingRectRight.left.toFloat() else maxX
+
+ minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
+ minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
+
+ cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
+ cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
+ }
+
+ // This makes sure that if we have an inset on one side of the screen, we mirror it on
+ // the other side. Since removing space from one of the max values messes with the scale,
+ // we also have to account for it using our min values.
+ if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
+ if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
+ if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
+ maxX -= (minX * 2)
+ } else if (minX > 0) {
+ maxX -= minX
+ }
+ if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
+ maxY -= (minY * 2)
+ } else if (minY > 0) {
+ maxY -= minY
+ }
+
+ return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
+ }
+
+ /**
+ * Initializes an InputOverlayDrawableButton, given by resId, with all of the
+ * parameters set for it to be properly shown on the InputOverlay.
+ *
+ *
+ * This works due to the way the X and Y coordinates are stored within
+ * the [SharedPreferences].
+ *
+ *
+ * In the input overlay configuration menu,
+ * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
+ * the X and Y coordinates of the button at the END of its touch event
+ * (when you remove your finger/stylus from the touchscreen) are then stored
+ * within a SharedPreferences instance so that those values can be retrieved here.
+ *
+ *
+ * This has a few benefits over the conventional way of storing the values
+ * (ie. within the yuzu ini file).
+ *
+ * * No native calls
+ * * Keeps Android-only values inside the Android environment
+ *
+ *
+ *
+ * Technically no modifications should need to be performed on the returned
+ * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
+ * for Android to call the onDraw method.
+ *
+ * @param context The current [Context].
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
+ * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
+ * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
+ * @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
+ */
+ private fun initializeOverlayButton(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ defaultResId: Int,
+ pressedResId: Int,
+ buttonId: Int,
+ orientation: String
+ ): InputOverlayDrawableButton {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on button ID and user preference
+ var scale: Float = when (buttonId) {
+ ButtonType.BUTTON_HOME,
+ ButtonType.BUTTON_CAPTURE,
+ ButtonType.BUTTON_PLUS,
+ ButtonType.BUTTON_MINUS -> 0.07f
+
+ ButtonType.TRIGGER_L,
+ ButtonType.TRIGGER_R,
+ ButtonType.TRIGGER_ZL,
+ ButtonType.TRIGGER_ZR -> 0.26f
+
+ else -> 0.11f
+ }
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableButton.
+ val defaultStateBitmap = getBitmap(context, defaultResId, scale)
+ val pressedStateBitmap = getBitmap(context, pressedResId, scale)
+ val overlayDrawable =
+ InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val xKey = "$buttonId$orientation-X"
+ val yKey = "$buttonId$orientation-Y"
+ val drawableXPercent = sPrefs.getFloat(xKey, 0f)
+ val drawableYPercent = sPrefs.getFloat(yKey, 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val width = overlayDrawable.width
+ val height = overlayDrawable.height
+
+ // Now set the bounds for the InputOverlayDrawableButton.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
+ overlayDrawable.setBounds(
+ drawableX - (width / 2),
+ drawableY - (height / 2),
+ drawableX + (width / 2),
+ drawableY + (height / 2)
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(
+ drawableX - (width / 2),
+ drawableY - (height / 2)
+ )
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+
+ /**
+ * Initializes an [InputOverlayDrawableDpad]
+ *
+ * @param context The current [Context].
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param defaultResId The [Bitmap] resource ID of the default state.
+ * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
+ * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
+ * @return the initialized [InputOverlayDrawableDpad]
+ */
+ private fun initializeOverlayDpad(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ defaultResId: Int,
+ pressedOneDirectionResId: Int,
+ pressedTwoDirectionsResId: Int,
+ orientation: String
+ ): InputOverlayDrawableDpad {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on button ID and user preference
+ var scale = 0.25f
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableDpad.
+ val defaultStateBitmap =
+ getBitmap(context, defaultResId, scale)
+ val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
+ val pressedTwoDirectionsStateBitmap =
+ getBitmap(context, pressedTwoDirectionsResId, scale)
+
+ val overlayDrawable = InputOverlayDrawableDpad(
+ res,
+ defaultStateBitmap,
+ pressedOneDirectionStateBitmap,
+ pressedTwoDirectionsStateBitmap,
+ ButtonType.DPAD_UP,
+ ButtonType.DPAD_DOWN,
+ ButtonType.DPAD_LEFT,
+ ButtonType.DPAD_RIGHT
+ )
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f)
+ val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val width = overlayDrawable.width
+ val height = overlayDrawable.height
+
+ // Now set the bounds for the InputOverlayDrawableDpad.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
+ overlayDrawable.setBounds(
+ drawableX - (width / 2),
+ drawableY - (height / 2),
+ drawableX + (width / 2),
+ drawableY + (height / 2)
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+
+ /**
+ * Initializes an [InputOverlayDrawableJoystick]
+ *
+ * @param context The current [Context]
+ * @param windowSize The size of the window to draw the overlay on.
+ * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
+ * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
+ * @param pressedResInner Resource ID for the pressed inner image of the joystick.
+ * @param joystick Identifier for which joystick this is.
+ * @param button Identifier for which joystick button this is.
+ * @return the initialized [InputOverlayDrawableJoystick].
+ */
+ private fun initializeOverlayJoystick(
+ context: Context,
+ windowSize: Pair<Point, Point>,
+ resOuter: Int,
+ defaultResInner: Int,
+ pressedResInner: Int,
+ joystick: Int,
+ button: Int,
+ orientation: String
+ ): InputOverlayDrawableJoystick {
+ // Resources handle for fetching the initial Drawable resource.
+ val res = context.resources
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
+ val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ // Decide scale based on user preference
+ var scale = 0.3f
+ scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
+ scale /= 100f
+
+ // Initialize the InputOverlayDrawableJoystick.
+ val bitmapOuter = getBitmap(context, resOuter, scale)
+ val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
+ val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
+
+ // Get the minimum and maximum coordinates of the screen where the button can be placed.
+ val min = windowSize.first
+ val max = windowSize.second
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f)
+ val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
+ val drawableX = (drawableXPercent * max.x + min.x).toInt()
+ val drawableY = (drawableYPercent * max.y + min.y).toInt()
+ val outerScale = 1.66f
+
+ // Now set the bounds for the InputOverlayDrawableJoystick.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
+ val outerSize = bitmapOuter.width
+ val outerRect = Rect(
+ drawableX - (outerSize / 2),
+ drawableY - (outerSize / 2),
+ drawableX + (outerSize / 2),
+ drawableY + (outerSize / 2)
+ )
+ val innerRect =
+ Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
+
+ // Send the drawableId to the joystick so it can be referenced when saving control position.
+ val overlayDrawable = InputOverlayDrawableJoystick(
+ res,
+ bitmapOuter,
+ bitmapInnerDefault,
+ bitmapInnerPressed,
+ outerRect,
+ innerRect,
+ joystick,
+ button
+ )
+
+ // Need to set the image's position
+ overlayDrawable.setPosition(drawableX, drawableY)
+ val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
+ overlayDrawable.setOpacity(savedOpacity * 255 / 100)
+ return overlayDrawable
+ }
+ }
+}